/*
 * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/EntityEnclosingMethod.java,v 1.39 2004/07/03 14:27:03 olegk Exp $
 * $Revision: 480424 $
 * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $
 *
 * ====================================================================
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.commons.httpclient.methods;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;

import org.apache.commons.httpclient.ChunkedOutputStream;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpConnection;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.HttpVersion;
import org.apache.commons.httpclient.ProtocolException;
import org.apache.commons.logging.Log;

/**
 * This abstract class serves as a foundation for all HTTP methods that can
 * enclose an entity within requests
 * 
 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
 * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
 * 
 * @since 2.0beta1
 * @version $Revision: 480424 $
 */
public abstract class EntityEnclosingMethod extends ExpectContinueMethod {

	// ----------------------------------------- Static variables/initializers

	/**
	 * The content length will be calculated automatically. This implies
	 * buffering of the content.
	 * 
	 * @deprecated Use {@link InputStreamRequestEntity#CONTENT_LENGTH_AUTO}.
	 */
	public static final long CONTENT_LENGTH_AUTO = InputStreamRequestEntity.CONTENT_LENGTH_AUTO;

	/**
	 * The request will use chunked transfer encoding. Content length is not
	 * calculated and the content is not buffered.<br>
	 * 
	 * @deprecated Use {@link #setContentChunked(boolean)}.
	 */
	public static final long CONTENT_LENGTH_CHUNKED = -1;

	/** LOG object for this class. */
	// private static final Log LOG =
	// LogFactory.getLog(EntityEnclosingMethod.class);

	/** The unbuffered request body, if any. */
	private InputStream requestStream = null;

	/** The request body as string, if any. */
	private String requestString = null;

	private RequestEntity requestEntity;

	/** Counts how often the request was sent to the server. */
	private int repeatCount = 0;

	/**
	 * The content length of the <code>requestBodyStream</code> or one of
	 * <code>CONTENT_LENGTH_AUTO</code> and <code>CONTENT_LENGTH_CHUNKED</code>.
	 * 
	 * @deprecated
	 */
	private long requestContentLength = InputStreamRequestEntity.CONTENT_LENGTH_AUTO;

	private boolean chunked = false;

	// ----------------------------------------------------------- Constructors

	/**
	 * No-arg constructor.
	 * 
	 * @since 2.0
	 */
	public EntityEnclosingMethod() {
		super();
		setFollowRedirects(false);
	}

	/**
	 * Constructor specifying a URI.
	 * 
	 * @param uri
	 *            either an absolute or relative URI
	 * 
	 * @since 2.0
	 */
	public EntityEnclosingMethod(String uri) {
		super(uri);
		setFollowRedirects(false);
	}

	/**
	 * Returns <tt>true</tt> if there is a request body to be sent.
	 * 
	 * <P>
	 * This method must be overridden by sub-classes that implement alternative
	 * request content input methods
	 * </p>
	 * 
	 * @return boolean
	 * 
	 * @since 2.0beta1
	 */
	protected boolean hasRequestContent() {
		// LOG.trace("enter EntityEnclosingMethod.hasRequestContent()");
		return (this.requestEntity != null) || (this.requestStream != null)
				|| (this.requestString != null);
	}

	/**
	 * Clears the request body.
	 * 
	 * <p>
	 * This method must be overridden by sub-classes that implement alternative
	 * request content input methods.
	 * </p>
	 * 
	 * @since 2.0beta1
	 */
	protected void clearRequestBody() {
		// LOG.trace("enter EntityEnclosingMethod.clearRequestBody()");
		this.requestStream = null;
		this.requestString = null;
		this.requestEntity = null;
	}

	/**
	 * Generates the request body.
	 * 
	 * <p>
	 * This method must be overridden by sub-classes that implement alternative
	 * request content input methods.
	 * </p>
	 * 
	 * @return request body as an array of bytes. If the request content has not
	 *         been set, returns <tt>null</tt>.
	 * 
	 * @since 2.0beta1
	 */
	protected byte[] generateRequestBody() {
		// LOG.trace("enter EntityEnclosingMethod.renerateRequestBody()");
		return null;
	}

	protected RequestEntity generateRequestEntity() {

		byte[] requestBody = generateRequestBody();
		if (requestBody != null) {
			// use the request body, if it exists.
			// this is just for backwards compatability
			this.requestEntity = new ByteArrayRequestEntity(requestBody);
		} else if (this.requestStream != null) {
			this.requestEntity = new InputStreamRequestEntity(requestStream,
					requestContentLength);
			this.requestStream = null;
		} else if (this.requestString != null) {
			String charset = getRequestCharSet();
			try {
				this.requestEntity = new StringRequestEntity(requestString,
						null, charset);
			} catch (UnsupportedEncodingException e) {
				// if (LOG.isWarnEnabled()) {
				// LOG.warn(charset + " not supported");
				// }
				try {
					this.requestEntity = new StringRequestEntity(requestString,
							null, null);
				} catch (UnsupportedEncodingException ignore) {
				}
			}
		}

		return this.requestEntity;
	}

	/**
	 * Entity enclosing requests cannot be redirected without user intervention
	 * according to RFC 2616.
	 * 
	 * @return <code>false</code>.
	 * 
	 * @since 2.0
	 */
	public boolean getFollowRedirects() {
		return false;
	}

	/**
	 * Entity enclosing requests cannot be redirected without user intervention
	 * according to RFC 2616.
	 * 
	 * @param followRedirects
	 *            must always be <code>false</code>
	 */
	public void setFollowRedirects(boolean followRedirects) {
		if (followRedirects == true) {
			throw new IllegalArgumentException(
					"Entity enclosing requests cannot be redirected without user intervention");
		}
		super.setFollowRedirects(false);
	}

	/**
	 * Sets length information about the request body.
	 * 
	 * <p>
	 * Note: If you specify a content length the request is unbuffered. This
	 * prevents redirection and automatic retry if a request fails the first
	 * time. This means that the HttpClient can not perform authorization
	 * automatically but will throw an Exception. You will have to set the
	 * necessary 'Authorization' or 'Proxy-Authorization' headers manually.
	 * </p>
	 * 
	 * @param length
	 *            size in bytes or any of CONTENT_LENGTH_AUTO,
	 *            CONTENT_LENGTH_CHUNKED. If number of bytes or
	 *            CONTENT_LENGTH_CHUNKED is specified the content will not be
	 *            buffered internally and the Content-Length header of the
	 *            request will be used. In this case the user is responsible to
	 *            supply the correct content length. If CONTENT_LENGTH_AUTO is
	 *            specified the request will be buffered before it is sent over
	 *            the network.
	 * 
	 * @deprecated Use {@link #setContentChunked(boolean)} or
	 *             {@link #setRequestEntity(RequestEntity)}
	 */
	public void setRequestContentLength(int length) {
		// LOG.trace("enter EntityEnclosingMethod.setRequestContentLength(int)");
		this.requestContentLength = length;
	}

	/**
	 * Returns the request's charset. The charset is parsed from the request
	 * entity's content type, unless the content type header has been set
	 * manually.
	 * 
	 * @see RequestEntity#getContentType()
	 * 
	 * @since 3.0
	 */
	public String getRequestCharSet() {
		if (getRequestHeader("Content-Type") == null) {
			// check the content type from request entity
			// We can't call getRequestEntity() since it will probably call
			// this method.
			if (this.requestEntity != null) {
				return getContentCharSet(new Header("Content-Type",
						requestEntity.getContentType()));
			} else {
				return super.getRequestCharSet();
			}
		} else {
			return super.getRequestCharSet();
		}
	}

	/**
	 * Sets length information about the request body.
	 * 
	 * <p>
	 * Note: If you specify a content length the request is unbuffered. This
	 * prevents redirection and automatic retry if a request fails the first
	 * time. This means that the HttpClient can not perform authorization
	 * automatically but will throw an Exception. You will have to set the
	 * necessary 'Authorization' or 'Proxy-Authorization' headers manually.
	 * </p>
	 * 
	 * @param length
	 *            size in bytes or any of CONTENT_LENGTH_AUTO,
	 *            CONTENT_LENGTH_CHUNKED. If number of bytes or
	 *            CONTENT_LENGTH_CHUNKED is specified the content will not be
	 *            buffered internally and the Content-Length header of the
	 *            request will be used. In this case the user is responsible to
	 *            supply the correct content length. If CONTENT_LENGTH_AUTO is
	 *            specified the request will be buffered before it is sent over
	 *            the network.
	 * 
	 * @deprecated Use {@link #setContentChunked(boolean)} or
	 *             {@link #setRequestEntity(RequestEntity)}
	 */
	public void setRequestContentLength(long length) {
		// LOG.trace("enter EntityEnclosingMethod.setRequestContentLength(int)");
		this.requestContentLength = length;
	}

	/**
	 * Sets whether or not the content should be chunked.
	 * 
	 * @param chunked
	 *            <code>true</code> if the content should be chunked
	 * 
	 * @since 3.0
	 */
	public void setContentChunked(boolean chunked) {
		this.chunked = chunked;
	}

	/**
	 * Returns the length of the request body.
	 * 
	 * @return number of bytes in the request body
	 */
	protected long getRequestContentLength() {
		// LOG.trace("enter EntityEnclosingMethod.getRequestContentLength()");

		if (!hasRequestContent()) {
			return 0;
		}
		if (this.chunked) {
			return -1;
		}
		if (this.requestEntity == null) {
			this.requestEntity = generateRequestEntity();
		}
		return (this.requestEntity == null) ? 0 : this.requestEntity
				.getContentLength();
	}

	/**
	 * Populates the request headers map to with additional
	 * {@link org.apache.commons.httpclient.Header headers} to be submitted to
	 * the given {@link HttpConnection}.
	 * 
	 * <p>
	 * This implementation adds tt>Content-Length</tt> or
	 * <tt>Transfer-Encoding</tt> headers.
	 * </p>
	 * 
	 * <p>
	 * Subclasses may want to override this method to to add additional headers,
	 * and may choose to invoke this implementation (via <tt>super</tt>) to add
	 * the "standard" headers.
	 * </p>
	 * 
	 * @param state
	 *            the {@link HttpState state} information associated with this
	 *            method
	 * @param conn
	 *            the {@link HttpConnection connection} used to execute this
	 *            HTTP method
	 * 
	 * @throws IOException
	 *             if an I/O (transport) error occurs. Some transport exceptions
	 *             can be recovered from.
	 * @throws HttpException
	 *             if a protocol exception occurs. Usually protocol exceptions
	 *             cannot be recovered from.
	 * 
	 * @see #writeRequestHeaders
	 * 
	 * @since 3.0
	 */
	protected void addRequestHeaders(HttpState state, HttpConnection conn)
			throws IOException, HttpException {
		// LOG.trace("enter EntityEnclosingMethod.addRequestHeaders(HttpState, "
		// + "HttpConnection)");

		super.addRequestHeaders(state, conn);
		addContentLengthRequestHeader(state, conn);

		// only use the content type of the request entity if it has not already
		// been
		// set manually
		if (getRequestHeader("Content-Type") == null) {
			RequestEntity requestEntity = getRequestEntity();
			if (requestEntity != null && requestEntity.getContentType() != null) {
				setRequestHeader("Content-Type", requestEntity.getContentType());
			}
		}
	}

	/**
	 * Generates <tt>Content-Length</tt> or <tt>Transfer-Encoding: Chunked</tt>
	 * request header, as long as no <tt>Content-Length</tt> request header
	 * already exists.
	 * 
	 * @param state
	 *            current state of http requests
	 * @param conn
	 *            the connection to use for I/O
	 * 
	 * @throws IOException
	 *             when errors occur reading or writing to/from the connection
	 * @throws HttpException
	 *             when a recoverable error occurs
	 */
	protected void addContentLengthRequestHeader(HttpState state,
			HttpConnection conn) throws IOException, HttpException {
		// LOG.trace("enter EntityEnclosingMethod.addContentLengthRequestHeader("
		// + "HttpState, HttpConnection)");

		if ((getRequestHeader("content-length") == null)
				&& (getRequestHeader("Transfer-Encoding") == null)) {
			long len = getRequestContentLength();
			if (len < 0) {
				if (getEffectiveVersion().greaterEquals(HttpVersion.HTTP_1_1)) {
					addRequestHeader("Transfer-Encoding", "chunked");
				} else {
					throw new ProtocolException(getEffectiveVersion()
							+ " does not support chunk encoding");
				}
			} else {
				addRequestHeader("Content-Length", String.valueOf(len));
			}
		}
	}

	/**
	 * Sets the request body to be the specified inputstream.
	 * 
	 * @param body
	 *            Request body content as {@link java.io.InputStream}
	 * 
	 * @deprecated use {@link #setRequestEntity(RequestEntity)}
	 */
	public void setRequestBody(InputStream body) {
		// LOG.trace("enter EntityEnclosingMethod.setRequestBody(InputStream)");
		clearRequestBody();
		this.requestStream = body;
	}

	/**
	 * Sets the request body to be the specified string. The string will be
	 * submitted, using the encoding specified in the Content-Type request
	 * header.<br>
	 * Example:
	 * <code>setRequestHeader("Content-type", "text/xml; charset=UTF-8");</code><br>
	 * Would use the UTF-8 encoding. If no charset is specified, the
	 * {@link org.apache.commons.httpclient.HttpConstants#DEFAULT_CONTENT_CHARSET
	 * default} content encoding is used (ISO-8859-1).
	 * 
	 * @param body
	 *            Request body content as a string
	 * 
	 * @deprecated use {@link #setRequestEntity(RequestEntity)}
	 */
	public void setRequestBody(String body) {
		// LOG.trace("enter EntityEnclosingMethod.setRequestBody(String)");
		clearRequestBody();
		this.requestString = body;
	}

	/**
	 * Writes the request body to the given {@link HttpConnection connection}.
	 * 
	 * @param state
	 *            the {@link HttpState state} information associated with this
	 *            method
	 * @param conn
	 *            the {@link HttpConnection connection} used to execute this
	 *            HTTP method
	 * 
	 * @return <tt>true</tt>
	 * 
	 * @throws IOException
	 *             if an I/O (transport) error occurs. Some transport exceptions
	 *             can be recovered from.
	 * @throws HttpException
	 *             if a protocol exception occurs. Usually protocol exceptions
	 *             cannot be recovered from.
	 */
	protected boolean writeRequestBody(HttpState state, HttpConnection conn)
			throws IOException, HttpException {
		// LOG.trace(
		// "enter EntityEnclosingMethod.writeRequestBody(HttpState, HttpConnection)");

		if (!hasRequestContent()) {
			// LOG.debug("Request body has not been specified");
			return true;
		}
		if (this.requestEntity == null) {
			this.requestEntity = generateRequestEntity();
		}
		if (requestEntity == null) {
			// LOG.debug("Request body is empty");
			return true;
		}

		long contentLength = getRequestContentLength();

		if ((this.repeatCount > 0) && !requestEntity.isRepeatable()) {
			throw new ProtocolException(
					"Unbuffered entity enclosing request can not be repeated.");
		}

		this.repeatCount++;

		OutputStream outstream = conn.getRequestOutputStream();

		if (contentLength < 0) {
			outstream = new ChunkedOutputStream(outstream);
		}

		requestEntity.writeRequest(outstream);

		// This is hardly the most elegant solution to closing chunked stream
		if (outstream instanceof ChunkedOutputStream) {
			((ChunkedOutputStream) outstream).finish();
		}

		outstream.flush();

		// LOG.debug("Request body sent");
		return true;
	}

	/**
	 * Recycles the HTTP method so that it can be used again. Note that all of
	 * the instance variables will be reset once this method has been called.
	 * This method will also release the connection being used by this HTTP
	 * method.
	 * 
	 * @see #releaseConnection()
	 * 
	 * @deprecated no longer supported and will be removed in the future version
	 *             of HttpClient
	 */
	public void recycle() {
		// LOG.trace("enter EntityEnclosingMethod.recycle()");
		clearRequestBody();
		this.requestContentLength = InputStreamRequestEntity.CONTENT_LENGTH_AUTO;
		this.repeatCount = 0;
		this.chunked = false;
		super.recycle();
	}

	/**
	 * @return Returns the requestEntity.
	 * 
	 * @since 3.0
	 */
	public RequestEntity getRequestEntity() {
		return generateRequestEntity();
	}

	/**
	 * @param requestEntity
	 *            The requestEntity to set.
	 * 
	 * @since 3.0
	 */
	public void setRequestEntity(RequestEntity requestEntity) {
		clearRequestBody();
		this.requestEntity = requestEntity;
	}

}
